//---------------------------------------------------------------------
//  This file is part of the Windows Workflow Foundation SDK Code Samples.
// 
//  Copyright (C) Microsoft Corporation.  All rights reserved.
// 
//This source code is intended only as a supplement to Microsoft
//Development Tools and/or on-line documentation.  See these other
//materials for detailed information regarding Microsoft code samples.
// 
//THIS CODE AND INFORMATION ARE PROVIDED AS IS WITHOUT WARRANTY OF ANY
//KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
//IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
//PARTICULAR PURPOSE.
//---------------------------------------------------------------------

///
/// Further modifications made by G. Watts;
///
/// This is a simple binary stash for the work flow persitancy program. It uses
/// the filesystem to store its data rather than an SQL database. Meant to be
/// light-weight. Does not support multiple run-times using the same location on the
/// file system (i.e. the lock parameter and status flag is universally ignored).
/// Also, does not support transactions explicitly.
/// 

using System;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.IO;
using System.Workflow.ComponentModel;
using System.Workflow.Runtime;
using System.Workflow.Runtime.Hosting;
using System.Threading;
using System.Linq;

namespace CommonWFLibrary
{
    /// <summary>
    /// Store WF persistancy data on disk. Just add this as a service.
    /// </summary>
    public class FilePersistenceService : WorkflowPersistenceService
    {
        public readonly static TimeSpan MaxInterval = new TimeSpan(30, 0, 0, 0);

        private bool unloadOnIdle = false;
        private Dictionary<Guid,Timer> instanceTimers;

        /// <summary>
        /// Get the location where the data files are cached.
        /// </summary>
        public DirectoryInfo CacheLocation { get; private set; }

        /// <summary>
        /// Create the persitancy service. Add this to a runtime's service list
        /// to turn on persistancy!
        /// </summary>
        /// <param name="unloadOnIdle">Controls if WF state should be saved when it is idle</param>
        /// <param name="dirLoc">Directory where things can be stored. Should be kept from run-to-run for persistancy reasons!</param>
        public FilePersistenceService(bool unloadOnIdle, DirectoryInfo dirLoc)
        {
            this.unloadOnIdle = unloadOnIdle;
            this.instanceTimers = new Dictionary<Guid, Timer>();
            CacheLocation = dirLoc;

            if (!CacheLocation.Exists)
            {
                CacheLocation.Create();
            }
        }

        /// <summary>
        /// Make sure none of our timers are around to fire any longer!
        /// </summary>
        protected override void OnStopped()
        {
            lock (this)
            {
                foreach (Guid key in instanceTimers.Keys)
                {
                    Timer t = instanceTimers[key];
                    t.Dispose();
                }
                instanceTimers.Clear();
            }
            base.OnStopped();
        }

        /// <summary>
        /// The prefix string for the files that we will store. To tell them apart from anything
        /// else that gets 
        /// </summary>
        private const string _prefixString = "WF-Data-";

        /// <summary>
        /// Look in the directory and see if there are any cahced guids. If so, we will
        /// just re-start those workflows.
        /// </summary>
        protected override void OnStarted()
        {
            base.OnStarted();

            if (CacheLocation.Exists)
            {
                var guids = from fname in CacheLocation.GetFiles()
                            where fname.Name.StartsWith(_prefixString)
                            select new Guid(fname.Name.Substring(_prefixString.Length));
                foreach (var g in guids)
                {
                    Runtime.GetWorkflow(g);
                }
            }
        }

        /// <summary>
        /// Save the workflow to disk. If this workflow is done, then we will
        /// delete its data from the disk. No need to keep that sort of old, dead
        /// data around!
        /// </summary>
        /// <param name="rootActivity"></param>
        /// <param name="unlock"></param>
        protected override void SaveWorkflowInstanceState(Activity rootActivity, bool unlock)
        {
            Guid contextGuid = (Guid)rootActivity.GetValue(Activity.ActivityContextGuidProperty);

            ///
            /// If this is terminating, delete any data on disk
            /// 

            WorkflowStatus status = GetWorkflowStatus(rootActivity);
            if (status == WorkflowStatus.Completed
                || status == WorkflowStatus.Terminated)
            {
                FileInfo dataFile = GenerateCachFileInfo(contextGuid);
                if (dataFile.Exists)
                {
                    dataFile.Delete();
                }
                return;
            }

            ///
            /// The workflow isn't going away -- so we do want to write it to disk!
            /// 

            SerializeToFile(
                WorkflowPersistenceService.GetDefaultSerializedForm(rootActivity), contextGuid);

            // See when the next timer (Delay activity) for this workflow will expire
            TimerEventSubscriptionCollection timers = (TimerEventSubscriptionCollection)rootActivity.GetValue(TimerEventSubscriptionCollection.TimerCollectionProperty);
            TimerEventSubscription subscription = timers.Peek();
            if (subscription != null)
            {
                // Set a system timer to automatically reload this workflow when its next timer expires
                TimerCallback callback = new TimerCallback(ReloadWorkflow);
                TimeSpan timeDifference = subscription.ExpiresAt - DateTime.UtcNow;
                // check to make sure timeDifference is in legal range
                if (timeDifference > FilePersistenceService.MaxInterval)
                {
                    timeDifference = FilePersistenceService.MaxInterval;
                }
                else if (timeDifference < TimeSpan.Zero)
                {
                    timeDifference = TimeSpan.Zero;
                }
                lock (this)
                {
                    this.instanceTimers.Add(contextGuid, new System.Threading.Timer(
                        callback,
                        subscription.WorkflowInstanceId,
                        timeDifference,
                        new TimeSpan(-1)));
                }
            }
        }

        /// <summary>
        /// The timer we were waiting for has fired. We should re-load this guy now to make sure
        /// it is around to receive the timer event from the runtime.
        /// </summary>
        /// <param name="id"></param>
        private void ReloadWorkflow(object id)
        {
            // Reload the workflow so that it will continue processing
            Timer toDispose;
            lock (this)
            {
                if (this.instanceTimers.TryGetValue((Guid)id, out toDispose))
                {
                    this.instanceTimers.Remove((Guid)id);
                    toDispose.Dispose();
                }
            }
            this.Runtime.GetWorkflow((Guid)id);
        }

        // Load workflow instance state.
        protected override Activity LoadWorkflowInstanceState(Guid instanceId)
        {
            byte[] workflowBytes = DeserializeFromFile(instanceId);
            return WorkflowPersistenceService.RestoreFromDefaultSerializedForm(workflowBytes, null);
        }

        // Unlock the workflow instance state.  
        // Instance state locking is necessary when multiple runtimes share instance persistence store
        protected override void UnlockWorkflowInstanceState(Activity state)
        {
            //File locking is not supported in this sample
        }

        // Save the completed activity state.
        protected override void SaveCompletedContextActivity(Activity activity)
        {
            Guid contextGuid = (Guid)activity.GetValue(Activity.ActivityContextGuidProperty);
            SerializeToFile(
                WorkflowPersistenceService.GetDefaultSerializedForm(activity), contextGuid);
        }

        // Load the completed activity state.
        protected override Activity LoadCompletedContextActivity(Guid activityId, Activity outerActivity)
        {
            byte[] workflowBytes = DeserializeFromFile(activityId);
            Activity deserializedActivities = WorkflowPersistenceService.RestoreFromDefaultSerializedForm(workflowBytes, outerActivity);
            return deserializedActivities;

        }

        protected override bool UnloadOnIdle(Activity activity)
        {
            return unloadOnIdle;
        }

        /// <summary>
        /// Given a bunch of bytes, write them to a file!
        /// </summary>
        /// <param name="workflowBytes"></param>
        /// <param name="id"></param>
        private void SerializeToFile(byte[] workflowBytes, Guid id)
        {
            FileInfo filename = GenerateCachFileInfo(id);
            FileStream fileStream = null;
            try
            {
                if (filename.Exists)
                {
                    filename.Delete();
                    filename.Refresh();
                }

                fileStream = filename.Create();

                // Get the serialized form
                fileStream.Write(workflowBytes, 0, workflowBytes.Length);
            }
            finally
            {
                if (fileStream != null)
                    fileStream.Close();
            }
        }

        /// <summary>
        /// Returns the name of the file that we will use for this particular Activity's guid.
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        private FileInfo GenerateCachFileInfo(Guid id)
        {
            return new FileInfo(CacheLocation.FullName + "\\" + _prefixString + id.ToString());
        }

        /// <summary>
        /// Read back in a workflow
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        private byte[] DeserializeFromFile(Guid id)
        {
            FileInfo filename = GenerateCachFileInfo(id);
            FileStream fileStream = null;
            try
            {
                // File opened for shared reads but no writes by anyone
                fileStream = filename.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
                fileStream.Seek(0, SeekOrigin.Begin);
                byte[] workflowBytes = new byte[fileStream.Length];

                // Get the serialized form
                fileStream.Read(workflowBytes, 0, workflowBytes.Length);

                return workflowBytes;
            }
            finally
            {
                fileStream.Close();
            }
        }

    }
}